﻿using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Windows.Controls;

namespace CryptoQrUtilities.Cryption
{
    /// <summary>
    /// Provides RSA services based on a crypto container name.
    /// Contains also API for maintaining available containers.
    /// </summary>
    public class ContainerBasedRsaProvider : IRsaProvider
    {
        private static readonly Dictionary<string, CspProviderFlags> s_containerStoreLocation = new Dictionary<string, CspProviderFlags>();

        /// <summary>
        /// Create a new instance of <see cref="ContainerBasedRsaProvider"/> initializing the name of the crypto container to use.
        /// </summary>
        /// <param name="containerName">The name of the required crypto container.</param>
        /// <param name="useOaepPadding">Whether to use OAEP padding when encryptin/decrypting.</param>
        public ContainerBasedRsaProvider(string containerName, bool useOaepPadding = true)
        {
            ContainerName = containerName;
            UseOaepPadding = useOaepPadding;
        }

        #region Static key-container maintenance

        /// <summary>
        /// Creates the RSA key container.
        /// </summary>
        /// <param name="containerName">The name of the key container for later use.</param>
        /// <param name="useMachineLevelStore">Whether this container should be in the machine-level store or the user-level.</param>
        /// <param name="keySize">The required RSA key size.</param>
        public static void CreateRsaPersistentKeyContainer(string containerName, bool useMachineLevelStore = true, int keySize = 1024)
        {
            if (s_containerStoreLocation.ContainsKey(containerName))
            {
                Logger.LogWarning("Trying to create RSA key container '{0}', it already exists in {1} store.", containerName, StoreLevelName(useMachineLevelStore));

                return;
            }

            try
            {
                var cspParams = new CspParameters(1)
                {
                    KeyContainerName = containerName,
                    Flags = StoreLevelFlag(useMachineLevelStore)
                };
                using (var rsaAlgorithm = new RSACryptoServiceProvider(keySize, cspParams))
                {
                    Logger.LogInfo("Created RSA persistent container '{0}' with key-size {1}.", containerName, rsaAlgorithm.KeySize);

                    rsaAlgorithm.Clear();
                }
            }
            catch (CryptographicException ex)
            {
                Logger.LogFatal("While creating persistent RSA key container '{0}', caught exception {1}", containerName, ex.ToString());

                throw;
            }

            s_containerStoreLocation.Add(containerName, StoreLevelFlag(useMachineLevelStore));
        }

        /// <summary>
        /// Deletes the given RSA key container from the store. This can be used to refresh the private RSA keys.
        /// </summary>
        /// <param name="containerName">The name of the container to remove.</param>
        public static void DeleteKeyFromContainer(string containerName)
        {
            VerifyRsaKeyContainerRegistered(containerName);

            try
            {
                var cspParams = new CspParameters(1)
                {
                    KeyContainerName = containerName,
                    Flags = s_containerStoreLocation[containerName]
                };
                using (var rsaAlgorithm = new RSACryptoServiceProvider(cspParams))
                {
                    rsaAlgorithm.PersistKeyInCsp = false;

                    rsaAlgorithm.Clear();

                    Logger.LogInfo("RSA key was deleted from the container '{0}'.", containerName);
                }
            }
            catch (CryptographicException ex)
            {
                Logger.LogFatal("While deleting persistent RSA key from container '{0}', caught exception {1}", containerName, ex.ToString());

                throw;
            }

            s_containerStoreLocation.Remove(containerName);
        }

        /// <summary>
        /// Verify that a crypto container is already registered and known.
        /// </summary>
        /// <param name="containerName">The name of the crpyto conrtainer for identification.</param>
        public static void VerifyRsaKeyContainerRegistered(string containerName)
        {
            if (!s_containerStoreLocation.ContainsKey(containerName))
            {
                throw new ArgumentException(string.Format("RSA key container '{0}' is not registered.", containerName));
            }
        }

        #endregion

        #region IRsaProvider implementation

        public RSACryptoServiceProvider GetRsaProvider()
        {
            VerifyRsaKeyContainerRegistered(ContainerName);

            var cspParams = new CspParameters(1) { KeyContainerName = ContainerName, Flags = s_containerStoreLocation[ContainerName] };

            return new RSACryptoServiceProvider(cspParams);
        }

        /// <summary>
        /// Gets whether to use OaepPadding.
        /// </summary>
        public bool UseOaepPadding { get; private set; }

        #endregion

        #region IEquatable implementation

        public bool Equals(IRsaProvider other)
        {
            var otherContainer = other as ContainerBasedRsaProvider;

            if (otherContainer != null)
            {
                return ContainerName.Equals(otherContainer.ContainerName) && UseOaepPadding == otherContainer.UseOaepPadding;
            }

            return Equals(this, other);
        }

        #endregion

        #region Object overrides

        public override int GetHashCode()
        {
            return ContainerName.GetHashCode() ^ UseOaepPadding.GetHashCode();
        }

        #endregion

        /// <summary>
        /// Gets the name of the .NET RSA container.
        /// </summary>
        public string ContainerName { get; private set; }

        #region Private helpers

        private static string StoreLevelName(bool isMachineLevel)
        {
            return isMachineLevel ? "Machine-level" : "User-level";
        }

        private static CspProviderFlags StoreLevelFlag(bool isMachineLevel)
        {
            return isMachineLevel ? CspProviderFlags.UseMachineKeyStore : CspProviderFlags.NoFlags;
        }

        #endregion
    }
}
